package calendar;

import edu.gmu.client.NamingServiceStub;
import edu.gmu.messages.EndPoint;
import edu.gmu.messages.ListManagerResponse;
import edu.gmu.naming.NamingService;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.Iterator;
import java.util.List;

/* this class manages all the calendars and allows creation and listing */
public class CalendarMgr implements CalendarMgrInt
{
    private static final long serialVersionUID = 3390578935L;
    
    private List calendars;
    private int id;
    
    private Type type;
    private EndPoint endpoint    = null;
    private int nextCalendarPort = -1;
    private volatile boolean stopped       = true;
    
    private NamingService namingService    = null;
    private CalendarMgrInt replica         = null;
    private CalendarMgrInt primary         = null;
    
    public CalendarMgr( )
    {
        
    }
    
    public CalendarMgr( int id,
                        EndPoint endpoint,
                        Type type,
                        NamingService namingService,
                        EndPoint primary )
    {
        this.id = id;
        
        this.calendars = new ArrayList();
        
        this.endpoint         = endpoint;
        this.nextCalendarPort = endpoint.port + 2;
        this.type             = type;
        this.namingService    = namingService;
        
        /*
         * Automatically try to read in old file
         */
        this.readFromDisk();
        
        
        if( this.type == Type.BACKUP )
        {
            this.primary = new CalendarMgrStub(primary);
        }
        
        /*
         * Before starting - go ahead and write out to file so that
         * we have something even if no events
         */
        this.writeToDisk();
        
        this.start();
        System.out.println("   INFO: Created CalendarMgr-"+id+"-"+type+" on "+endpoint);
    }
    
    public int getId()
    {
        return this.id;
    }
    
    public EndPoint getEndPoint()
    {
        return this.endpoint;
    }
    
    public NamingService getNamingService()
    {
        return this.namingService;
    }
    
    public void setNamingService(NamingService namingService)
    {
        this.namingService = namingService;
    }
    
    public void replicate()
    {
        // Migrate the server if we have to
        if( this.replica == null && this.type == Type.PRIMARY )
        {
            try
            {
                /*
                 * Replicate to the next server
                 */
                this.replica = this.migrate();
            }
            catch( IOException ioe )
            {
                // ignore - will try again later
                System.out.println( "PROBLEM: Calendar-"+this.id+" Unable to replicate" );
                //ioe.printStackTrace();
            }
        }
        
        // Connect calendars if we have to - this also handles new calendars!!!
        if( this.replica != null )
        {
            try
            {
                List<User> replicatedUsers = this.replica.list();
                
                // If this worked - tell the calendars to connect to replica
                // If one fail - all fail
                //boolean calendarsConnected = false;
                // For now, assume all went well :)
                for (int i = 0; i < calendars.size(); i++)
                {
                    CalendarObj calendarObj = (CalendarObj)calendars.get(i);
                    User user               =  calendarObj.getUser();
                    for (int j = 0; j < replicatedUsers.size(); j++)
                    {
                        User replicatedUser = replicatedUsers.get(j);
                        if( user.equals(replicatedUser) )
                        {
                            calendarObj.connectReplica(replicatedUser.getEndPoint());
                            //System.out.println( "Connecting to replica: " + replicatedUser );
                            break;
                        }
                    }
                }
            }
            catch( IOException ioe )
            {
                // ignore - will try again later
                System.out.println( "PROBLEM: Calendar-"+this.id+" Unable to connect calendars to replicas" );
                //ioe.printStackTrace();
            }
        }
    }
    
    public CalendarMgrInt migrate( ) throws IOException
    {
        // SanityCheck
        if( this.getType() != Type.PRIMARY )
        {
            System.out.println("Ignoring request to replicate primary "
                    +this.getFileName());
        }
        
        //--------------------------------------------------------------------//
        /*
         * Heuristic - have an affinity for your natural "home"
         */
        EndPoint nextNamingService = null;
        try
        {
            EndPoint next = this.namingService.getNextNamingService();
            // Make sure do not try to connect to self! NS is single threaded!!!
            while( next.equals(this.namingService.getEndPoint()) == false )
            {
                NamingServiceStub stub = new NamingServiceStub(next);
                ListManagerResponse response = stub.getManagers();
                if( response.namingServiceId == this.id  )
                {
                    System.out.println("FOUND: Home for "+this.id+" on " + response.localhost);
                    nextNamingService = response.localhost;
                    break;
                }
                next = response.next;
            }
        }
        catch(IOException ioe)
        {
            // ignore
            //ioe.printStackTrace();
        }
        //--------------------------------------------------------------------//

        
        if( nextNamingService == null )
        {
            nextNamingService = this.namingService.getNextNamingService();
        }
        
        NamingServiceStub nextService = new NamingServiceStub(nextNamingService);
        
        
        String fileName = this.getFileName();
        FileInputStream fis = new FileInputStream(fileName);
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        int b = fis.read();
        while( b != -1 )
        {
            baos.write(b);
            b = fis.read();
        }
        fis.close();
        
        CalendarMgrStub calMgrstub = nextService.replicate( this,
                                                            baos.toByteArray() );
        
        return calMgrstub;
    }
    
    /**
     * Ping our peer - if we are a primary and replica dies, reconnect to
     * next server and replicate.  If we are a backup and primary dies
     * we become the primary and will eventually replicate.
     */
    public void pingReplica()
    {
        if( this.type == Type.PRIMARY && this.replica != null )
        {
            try
            {
                this.replica.isAlive();
            }
            catch (IOException ex)
            {
                this.replica = null;
                System.out.println( "  ISSUE: CalendarMgr-"+id+"-"
                        +type+" lost connection to backup" );
                /* disconnect calendars from their replicas */
                for (int i = 0; i < calendars.size(); i++)
                {
                    CalendarObj calendar = (CalendarObj)calendars.get(i);
                    calendar.disconnectReplica();
                }
            }
        }
        else if( this.type == Type.BACKUP && this.primary != null )
        {
            try
            {
                this.primary.isAlive();
            }
            catch (IOException ex)
            {
                this.primary = null;
                System.out.println( "  ISSUE: CalendarMgr-"+id+"-"
                        +type+" lost connection to primary, backup now primary" );
                // notify clients to reconnect
                Iterator itr = calendars.iterator();
                while ( itr.hasNext() )
                {
                  CalendarObj calendarObj = (CalendarObj)itr.next();
                  calendarObj.notifyReconnect();
                }
                type = Type.PRIMARY;
            }
        }
    }
    
    /**
    * Gets the Calendar for specified user or null if none found
    * @param user
    * @return calendar or null
    */
    public CalendarObj getCalendar( User user )
    {
        CalendarObj calendar = null;
        for (int i = 0; i < calendars.size(); i++)
        {
            CalendarObj calendarObj = (CalendarObj)calendars.get(i);
            if( calendarObj.getUser().equals(user) )
            {
                calendar = calendarObj;
            }
        }
        return calendar;
    }
    
    /**
     * Gets the type of the CalendarMgr (all Calendars are implicitly this type)
     * @return type
     */
    public Type getType()
    {
        return this.type;
    }


    // dummy method for responding to pingReplica
    public void isAlive()
    {

    }

    /* create a new calendar for the specified user */
    public synchronized boolean createCalendar( User user ) throws IOException
    {
        if( this.replica != null && this.type == Type.PRIMARY)
        {
            this.replica.createCalendar(user);
        }
        
        if ( user.getName().compareTo("") == 0 )
        {
            return false;
        }

        CalendarObj previousCalendar = this.getCalendar(user);
        if( previousCalendar != null )
        {
            // User already exists
            return false;
        }

        CalendarObj calendar = new CalendarObj( user, this );
        calendars.add( calendar );
        System.out.println("   INFO:     Created Calendar-"+user+" on "+endpoint);
        //String strName = "Calendar" + calendar.getUser().getName();

        this.nextCalendarPort+=2;
        EndPoint calEndpoint = new EndPoint( this.endpoint.address, this.nextCalendarPort);
        user.setEndPoint( calEndpoint ); /*not sure if this is working */
        CalendarSkeleton stub = new CalendarSkeleton(   calendar,
                                                        calEndpoint,
                                                        calEndpoint.address );
        if ( stub == null )
        {
            System.out.println( "(ERROR) Failed to create CalendarSkeleton stub."
            + "Please try again." );
            calendars.remove( (Object)calendar );
            calendar = null;
            user.setEndPoint( null );
            return false;
        }
        calendar.setCallback( stub );
        //    System.out.println( "Creating calendar object skeleton on " + this.port );

        /* notify all connected clients that a new calendar has been created */
        for (int i = 0; i < calendars.size(); i++)
        {
            CalendarObj aCalendar = (CalendarObj)calendars.get(i);
            if ( this.type == Type.PRIMARY )
            {
                aCalendar.notifyClientUser( user, this.list() );
            }
        }
        this.writeToDisk();
        return true;
    }

    /* connect to the calendar specified by the user id */
    public synchronized boolean connectCalendar( User user ) throws IOException
    {
        CalendarObj calendar = this.getCalendar(user);
        
        // Reject if no calendar for user or calendar already connected
        if( calendar == null || calendar.isConnected() == true )
        {
            return false;
        }
        
        calendar.setIsConnected(true);
        
        return true;
    }

    /* disconnect from the calendar specified by the user id */
    public synchronized boolean disconnectCalendar( User user ) throws IOException
    {
        CalendarObj calendar = this.getCalendar(user);
        
        // Reject if no calendar for user or calendar already connected
        if( calendar == null || calendar.isConnected() == false )
        {
            return false;
        }
        
        calendar.setIsConnected(false);
        
        return true;
    }

    /* list all the calendars (user) that have been created */
    public synchronized ArrayList list() throws IOException
    {
        ArrayList users = new ArrayList();
        for (int i = 0; i < calendars.size(); i++)
        {
            CalendarObj calendar = (CalendarObj)calendars.get(i);
            users.add( calendar.getUser() );
        }
        return users;
    }
    
    //------------------------------------------------------------------------//
    // Notification code
    //------------------------------------------------------------------------//
    public void start()
    {
        this.stopped = false;
        /*
         * Handles in separate thread
         */
        Runnable serverTask = new Runnable()
        {
            public void run()
            {   
                while( stopped == false )
                {
                    notifyClients( );
                    replicate();
                    pingReplica();
                    try
                    {
                        Thread.sleep( 900 );
                    }
                    catch(InterruptedException ie)
                    {
                        // ignore
                    }
                }
            }
        };
        Thread serverThread = new Thread( serverTask );
        serverThread.setName( "NotifyClientsThread" );
        serverThread.start();
    }
    
    public void stop()
    {
        this.stopped = true;
    }
    
    public void notifyClients()
    {
        if( this.type == Type.BACKUP )
        {
            return;
        }
        
        /* loops indefinitely, alerting clients of important events */
        try
        {
            Calendar cal = Calendar.getInstance();
            //cal.add( Calendar.MINUTE, -4 ); /* time adjustment for testing */
            Date now        = cal.getTime();
            ArrayList users = this.list();
            for( int i = 0; i < users.size(); i++ )
            {
                User u               = (User)users.get(i);
                CalendarObj calendar = this.getCalendar(u);
                ArrayList events     = calendar.getEvents();
                Iterator itrEvents   = events.iterator();
                while ( itrEvents.hasNext() )
                {
                    Event e = (Event)itrEvents.next();
                    long diff = e.getStart().getTime() - now.getTime();
                    if( Math.abs(diff) <= 900 )
                    {
                        if ( !e.getIsSent() )
                        {
                            //System.out.println( "notify client of appt in calendarmgr loop" );
                            calendar.notifyClientEvent(e);
                        }
                    }
                }
            }
            Thread.sleep( 1000 );
        }
        catch(IOException ex)
        {
            System.out.println( "CalendarServer: main loop -> " + ex );
        }
        catch(Exception ex)
        {
            ex.printStackTrace();
        }
    }

    //------------------------------------------------------------------------//
    // Persistence code
    //------------------------------------------------------------------------//
    
    public static String getFileName( int id, Type type )
    {
        return "CalendarMgr-"+id+"-"+type+".data";
    }
    
    public String getFileName()
    {
        return getFileName(this.id, this.type);
    }
    

    /* write out the calendar manager and all calendars to disk (persistence) */
    public synchronized void writeToDisk()
    {
        try
        {
            write();
        }
        catch( Exception ex )
        {
            System.out.println( "CalendarMgr: writeToDisk -> " + ex );
        }
    }
    
    /**
     * Saves the state of this object to file.
     * @throws java.io.IOException
     */
    private void write() throws IOException
    {
        ObjectOutputStream e = new ObjectOutputStream(
        //XMLEncoder e = new XMLEncoder(
                          new BufferedOutputStream(
                              new FileOutputStream(this.getFileName())));
        e.writeObject(new Integer(this.id) );
        e.writeObject(this.calendars);
        e.close();
    }
    
    private synchronized void readFromDisk()
    {
        try
        {
            // Does the file even exist?
            File file = new File( this.getFileName() );
            if( file.exists() == true )
            {
                read();
            }
        }
        catch( Exception ex )
        {
            System.out.println( "CalendarMgr: readFromDisk -> " + ex );
        }
    }
    
    /**
     * Restores the state of this object from file.
     * @throws java.io.IOException
     */
    private void read() throws IOException, ClassNotFoundException
    {
        ObjectInputStream d = new ObjectInputStream(
        //XMLDecoder d = new XMLDecoder(
                          new BufferedInputStream(
                              new FileInputStream(this.getFileName())));
        Object oNextEventId = d.readObject();
        this.id = ((Integer)oNextEventId).intValue();
        Object result = d.readObject();
        this.calendars = (List)result;
        d.close();
        
        //--------------------------------------------------------------------//
        // Reset
        //--------------------------------------------------------------------//
        for (int i = 0; i < calendars.size(); i++)
        {
            CalendarObj calendar = (CalendarObj)calendars.get(i);
            calendar.setManager( this );
            calendar.clearMembers();
            String strName = null;
            this.nextCalendarPort+=2;
            EndPoint userEndpoint = new EndPoint( this.endpoint.address, this.nextCalendarPort );
            System.out.println( "   INFO:     Created (read) Calendar-"+calendar.getUser()+" on " + userEndpoint );
            calendar.getUser().setEndPoint(userEndpoint);
            CalendarSkeleton stub = new CalendarSkeleton(   calendar,
                                                            userEndpoint,
                                                            strName );
            calendar.setCallback( stub );   
            //  System.out.println( "Creating calendar object skeleton on " + this.port );
        }
    }
}